package com.cnvp.paladin.utils.beetl.fn;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;

/**
 * 类XssEncodeUtils，用于防御XSS，对特殊字符进行转义
 * 
 * 触发XSS的几个点及其转义方法可以归纳为： <br />
 * 1、作为js代码，输出在script标签中，不会输出到DOM <br />
 * 2、作为js代码，输出在script标签中，会输出到DOM <br />
 * 3、作为js代码，输出在js事件属性中，不会输出到DOM <br />
 * 4、作为js代码，输出在js事件属性中，会输出到DOM <br />
 * 5、输出在值为url的属性中 <br />
 * 6、输出在HTML文档中的其他位置 <br />
 * 
 *
 * @author door2guest
 * @version 2013-6-24
 */
public class XssEncodeUtils {
    
    /**
     * 单个字符JS hex编码（\x3C）长度
     */
    private static int JS_HEX_ENCODE_LENGTH = 4;
    /**
     * 单个字符js 普通编码（\<）的长度
     */
    private static int JS_NORMAL_ENCODE_LENGTH = 2;
    /**
     * 单个字符js unicode编码（\u003c）的长度
     */
    private static int JS_UNICODE_ENCODE_LENGTH = 6;
    

    /**
     * 做URL编码  <br />
     * 
     * <b>适合场景：仅适合url中参数名和参数值的编码</b>
     * 
     * @param value
     * @param charset
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String urlEncode(String value, String charset) throws UnsupportedEncodingException {
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        
        return URLEncoder.encode(value, charset);
    }
    
    public static String urlEncode(String value) throws UnsupportedEncodingException {
        return urlEncode(value, "utf-8");
    }
    
    /**
     * 将参数中的key 和 value 做url编码
     * 
     * @param keyValues
     * @return
     * @throws UnsupportedEncodingException
     */
    protected static String paramsUrlEncode(String keyValues) throws UnsupportedEncodingException {
        if (StringUtils.isEmpty(keyValues)) {
            return keyValues;
        }
        StringBuilder buff = new StringBuilder();
        String[] params = keyValues.split("&");
        for (String paramStr : params) {
            if (StringUtils.isEmpty(paramStr)) {
                continue ;
            }
            int pos = paramStr.indexOf("=");
            String key = "";
            String value = "";
            if (pos >= 0) {
                key = paramStr.substring(0, pos);
                if (pos >= 1) {
                    value = (pos > 0) ? paramStr.substring(pos+1) : "";
                }
            } else {
                key = paramStr;
            }
            buff.append(urlEncode(key) + "=" + urlEncode(value) + "&");
        }
        // 移除最后一个&
        if (buff.toString().endsWith("&")) {
            buff.deleteCharAt(buff.length() - 1);
        }
        
        return buff.toString();
    }
    
    /**
     * 
     * 做HTML实体编码。除了数字、字母、空白字符外的所有可见字符均转义成&#xXX形式  <br />
     * 
     * <b>适合场景：输出为html标签内容、标签名、属性名、属性值（js事件除外）。</b>
     * 
     * @param value
     * @return
     */
    public static String htmlEncode(String value) {
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        
        StringBuffer buff = new StringBuffer();
        for (int i = 0; i < value.length(); i++) {
            char ch = value.charAt(i);
            if (Character.isWhitespace(ch) && ch != ' ') {
                buff.append(ch);
            } else if (Character.isLetterOrDigit(ch)) {
                buff.append(ch);
            } else if (isVisibleChar(ch)) {
                buff.append("&#x" + Integer.toHexString((int) ch).toUpperCase() + ";");
            } else {
                buff.append(ch);
            }
        }
        
        return buff.toString();
    }
    
    /**
     * 做js编码。除了数字、字母、空白符以外的所有可见字符均加上\  <br />
     * 
     * <b>适合场景：直接输出在script标签内，且不会因document.write/obj.innerHTML等形式影响DOM</b>
     * 
     * @param value
     * @return
     */
    public static String jsEncode(String value) {
        if (StringUtils.isEmpty(value)) {
            return null;
        }
        
        StringBuffer buff = new StringBuffer();
        for (int i = 0; i < value.length(); i++) {
            char ch = value.charAt(i);
            if (Character.isWhitespace(ch)) {
                buff.append(ch);
            } else if (Character.isLetterOrDigit(ch)) {
                buff.append(ch);
            } else if (isVisibleChar(ch)) {
                buff.append("\\" + ch + "");
            } else {
                buff.append(ch);
            }
        }
        
        return buff.toString();
    }
    

    /**
     * 对输出为简单html的字符进行编码。  <br />
     * 
     * <b>适合场景：输出为html标签内容、标签名、属性名、属性值（js事件除外、url除外）。</b>
     * 
     * @see #htmlEncode(String)
     * @param value
     * @return
     */
    public static String encodeStrInHTML(String value) {
        return htmlEncode(value);
    }
    
    /**
     * 对输出为url的字符进行编码。这里只会将url中的 查询参数 && hash 进行编码<br />
     * 
     * <b>适合场景：URL，且是html标签属性的值，比如：img标签的src属性、iframe的src属性</b>
     * 
     * @param url 
     * @param charset
     * @return
     * @throws UnsupportedEncodingException
     * @throws MalformedURLException
     */
    public static String encodeStrInUrlAttr(String url, String charset) throws UnsupportedEncodingException, MalformedURLException {
        URL _url = new URL(url);
        StringBuilder buff = new StringBuilder();
        buff.append(_url.getProtocol() + "://").append(_url.getHost());
        if (_url.getPort() >= 0) {
            buff.append(":").append(_url.getPort());
        }
        buff.append(_url.getPath());
        if (StringUtils.isNotEmpty(_url.getQuery())) {
            buff.append("?").append(paramsUrlEncode(_url.getQuery()));
        }
        if (StringUtils.isNotEmpty(_url.getRef())) {
            buff.append("#").append(paramsUrlEncode(_url.getRef()));
        }
        
        return htmlEncode(buff.toString());
    }
    
    
    /**
     * 对输出为url的字符进行编码。这里只会将url中的 查询参数 && hash 进行编码<br />
     * 最后编码使用urlencode，适合url跳转
     * 
     * 保留http或者https协议  以及url path中的 /
     * 
     * <b>适合场景：URL，且是html标签属性的值，比如：img标签的src属性、iframe的src属性</b>
     * 
     * @param url 
     * @param charset
     * @return
     * @throws UnsupportedEncodingException
     * @throws MalformedURLException
     * @author add by huqun 
     */
    public static String urlEncodeStrInUrlAttr(String url, String charset) throws UnsupportedEncodingException, MalformedURLException {
        URL _url = new URL(url);
        StringBuilder buff = new StringBuilder();
        buff.append(_url.getProtocol() + "://").append(_url.getHost());
        if (_url.getPort() >= 0) {
            buff.append(":").append(_url.getPort());
        }
        // 对整个url路径进行urlEncode 保留URL path 中的 "/"
        buff.append(com.cnvp.paladin.utils.beetl.fn.URLEncoder.pathEncode(_url.getPath()));
        if (StringUtils.isNotEmpty(_url.getQuery())) {
            buff.append("?").append(paramsUrlEncode(_url.getQuery()));
        }
        if (StringUtils.isNotEmpty(_url.getRef())) {
            buff.append("#").append(paramsUrlEncode(_url.getRef()));
        }
        
        return buff.toString();
    }
    
    /**
     * 对输出为url的字符进行编码。这里只会将url中的 查询参数 && hash 进行编码<br />
     * 最后编码使用urlencode，适合url跳转
     * 
     * 保留http或者https协议  以及url path中的 /
     * 
     * <b>适合场景：URL，且是html标签属性的值，比如：img标签的src属性、iframe的src属性</b>
     * 
     * @param url
     * @return
     * @throws UnsupportedEncodingException
     * @throws MalformedURLException
     * @author add by huqun
     */
    public static String urlEncodeStrInUrlAttr(String url) throws UnsupportedEncodingException, MalformedURLException {
        return urlEncodeStrInUrlAttr(url, "utf-8");
    }
    

    /**
     * 对输出为url的字符进行编码。这里只会将url中的 查询参数 && hash 进行编码<br />
     * 
     * <b>适合场景：URL，且是html标签属性的值，比如：img标签的src属性、iframe的src属性</b>
     * 
     * @param url
     * @return
     * @throws UnsupportedEncodingException
     * @throws MalformedURLException
     */
    public static String encodeStrInUrlAttr(String url) throws UnsupportedEncodingException, MalformedURLException {
        return encodeStrInUrlAttr(url, "utf-8");
    }
    
    /**
     * 对输出为伪协议的字符进行编码。<br />
     * 
     * <b>适合场景：伪协议，比如：a标签的href属性，例如：&lt;a href="javascript:output"&gt;&lt;/a&gt;</b>
     * 
     * @param value
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String encodeStrInPseudoProtocol(String value) throws UnsupportedEncodingException {
        return htmlEncode(urlEncode(jsEncode(value)));
    }
    
    /**
     * 对输出在script标签内的字符进行编码。 如果包含方法名、变量名，则会导致语法错误。 <br />
     * 
     * <b>适合场景：直接输出在script标签内，且不会因document.write/obj.innerHTML等形式影响DOM</b>
     * 
     * @see #jsEncode(String)
     * @param value
     * @return
     */
    public static String encodeStrInScriptTag(String value) {
        return jsEncode(value);
    }
    
    /**
     * 对输出在script标签内，且js又会将其输出至DOM的字符进行编码。如果包含方法名、变量名，则会导致语法错误。<br />
     * 
     * <b>适合场景：输出在script标签内，且后续会因document.wirte()/obj.innerHTML等形式影响到DOM，但是js中无法正常解析，因为已经被html编码。<br /></b>
     * 一个更好的替代方式是：后端java输出时，用encodeStrInScriptTag进行编码；js输出到DOM时，再有js做html编码。
     * 
     * @param value
     * @return
     */
    public static String encodeStrInScriptTagOutputToDOM(String value) {
        return jsEncode(htmlEncode(value));
    }
    
    /**
     * 判断是否为合法的js变量名称
     * 
     * @param value
     * @return
     */
    public static boolean isLegalJSName(String value) {
        if (StringUtils.isEmpty(value)) {
            return false;
        }
        
        return Pattern.matches("^[a-zA-Z_$][0-9a-zA-Z_$]*$", value);
    }
        
    /**
     * 对输出在js事件中的字符进行编码（不会被输出至DOM）。将参数先js编码，再html编码。   <br />
     * 
     * <b>适合场景：输出在js事件中，且后续不会因document.wirte()/obj.innerHTML等形式影响到DOM。</b>
     * 
     * @param value
     * @return
     */
    public static String encodeStrInJsEventAttr(String value) {
        return htmlEncode(jsEncode(value));
    }
        
    
    /**
     * 对输出在js事件中、且会被js输出至DOM的字符进行编码。先将参数html编码，再js编码，在html编码。  <br />
     * 
     * <b>适合场景：输出在js事件中，且后续会因document.write/obj.innerHTML等形式影响到DOM。</b>
     * 
     * @param value
     * @return
     */
    public static String encodeStrInJsEventAttrOutputToDOM(String value) {
        return htmlEncode(jsEncode(htmlEncode(value)));
    }
    
    /**
     * 对输出在style标签中的css进行编码。
     * 
     * <b>适合场景：输出在style标签中，比如：&lt;style&gt;output&lt;/style&gt;。</b>
     * 
     * @param value
     * @return
     */
    public static String encodeStrInStyleTag(String value) {
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        
        value = removeStyleUnexpectedStr(value);
        
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < value.length(); i++) {
            char ch = value.charAt(i);
            if (ch == '<' || ch == '>' || ch == '/' || ch == ';') {
                // 转义
                buf.append("%" + Integer.toHexString((int) ch).toUpperCase());
            } else if (ch == '\r' || ch == '\n') {
                // 丢掉
            }else {
                buf.append(ch);
            }
        }
        return buf.toString();
    }
    
    /**
     * 对输出在标签的style属性里面的css进行编码。
     * 
     * <b>适合场景：输出在标签的style属性里面的css，比如：&lt;div style="output"&gt;&lt;/div&gt;</b>
     * 
     * @param value
     * @return
     */
    public static String encodeStrInStyleAttr(String value) {
        return htmlEncode(encodeStrInStyleTag(value));
    }
    
    private static String removeStyleUnexpectedStr(String value) {
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < value.length(); i++) {
            char ch = value.charAt(i);
            int code = (int)ch;
            if (code == 0) {
                // 丢弃
            } else {
                buf.append(ch);
            }
        }
        value = buf.toString();
        value = value.replaceAll("expression", "");
        value = value.replaceAll("ｅｘｐｒｅｓｓｉｏｎ", "");
        return value;
    }
    
    /**
     * 判断value从startIndex位置开始后六位是不是js unicode编码格式，形如 \u003C
     * @param value
     * @param startIndex
     * @return
     */
    protected static boolean isJSUniEncode(String value, int startIndex) {
        if (StringUtils.isEmpty(value) || startIndex < 0) {
            return false;
        }
        
        if (value.length() < startIndex + JS_UNICODE_ENCODE_LENGTH) {
            return false;
        }
        
        String unicodeStr = value.substring(startIndex, startIndex + JS_UNICODE_ENCODE_LENGTH);
        if (!unicodeStr.startsWith("\\u") && !unicodeStr.startsWith("\\U")) {
            return false;
        }
        for (int i = 2; i < unicodeStr.length(); i++) {
            char ch = unicodeStr.charAt(i);
            if (ch >= 'a' && ch <= 'f') {
                continue ;
            } else if (ch >= 'A' && ch <= 'F') {
                continue ;
            } else if (Character.isDigit(ch)) {
                continue ;
            } else {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * 判断value从startIndex开始后4位是否为js hex编码格式，形如：\x3C
     * @param value
     * @param startIndex
     * @return
     */
    protected static boolean isJsHexEncode(String value, int startIndex) {
        if (StringUtils.isEmpty(value) || startIndex < 0) {
            return false;
        }
        
        if (value.length() < startIndex + JS_HEX_ENCODE_LENGTH) {
            return false;
        }
        
        String hexStr = value.substring(startIndex, startIndex + JS_HEX_ENCODE_LENGTH);
        if (!hexStr.startsWith("\\x") && !hexStr.startsWith("\\X")) {
            return false;
        }
        for (int i = 2; i < hexStr.length(); i++) {
            char ch = hexStr.charAt(i);
            if (ch >= 'a' && ch <= 'f') {
                continue ;
            } else if (ch >= 'A' && ch <= 'F') {
                continue ;
            } else if (Character.isDigit(ch)) {
                continue ;
            } else {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * 检查value从startIndex开始后2位是否为js转码，格式为\<，第二位必须是可见字符
     * @param value
     * @param startIndex
     * @return
     */
    protected static boolean isJsNormalEncode(String value, int startIndex) {
        if (StringUtils.isEmpty(value) || startIndex < 0) {
            return false;
        }
        
        if (value.length() < startIndex + JS_NORMAL_ENCODE_LENGTH) {
            return false;
        }
        
        String hexStr = value.substring(startIndex, startIndex + JS_NORMAL_ENCODE_LENGTH);
        if (!hexStr.startsWith("\\") && !hexStr.startsWith("\\")) {
            return false;
        }
        // 如果是可见字符，则是js普通编码
        char ch = hexStr.charAt(1);
        if (isVisibleChar(ch)) {
            return true;
        }
        // 如果不是可见字符，则不是js普通编码
        return false;
    }
    
    /**
     * 是否为可见字符，不包括所有的空白符
     * @param ch
     * @return
     */
    protected static boolean isVisibleChar(char ch) {
        if ((int) ch >= 32 && (int) ch <= 126) {
            return true;
        }
        
        return false;
    }
        
    /**
     * 将json串使用js编码，不会影响原有的json结构和语义。前端必须使用eval('(' + escapedJson + ')')解析json。<br />
     * 
     * 如果其中某些字符已编码（&#x5c;xXX or &#x5c;uXXXX or &#x5c;X）则不会再次编码。
     * 
     * @param value
     * @return
     */
    public static String jsonJsEncode(String value) {
        if (StringUtils.isEmpty(value)) {
            return null;
        }
        
        StringBuffer buff = new StringBuffer();
        for (int i = 0; i < value.length(); i++) {
            char ch = value.charAt(i);
            if (isJsNormalEncode(value, i)) {
                buff.append(value.substring(i, i+JS_NORMAL_ENCODE_LENGTH));
                i = i + JS_NORMAL_ENCODE_LENGTH - 1;
            } else if (isJsHexEncode(value, i)) {
                buff.append(value.substring(i, i+JS_HEX_ENCODE_LENGTH));
                i = i + JS_HEX_ENCODE_LENGTH - 1;
            } else if (isJSUniEncode(value, i)) {
                buff.append(value.substring(i, i+JS_UNICODE_ENCODE_LENGTH));
                i = i + JS_UNICODE_ENCODE_LENGTH - 1;
            } else {
                buff.append(jsEncode(ch + ""));
            }
        }
        
        return buff.toString();
    }
}